{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b01237a8",
   "metadata": {},
   "source": [
    "## 添加消息历史记录（内存）\n",
    "\n",
    "RunnableWithMessageHistory 允许我们将消息历史记录添加到某些类型的链中。它包装另一个 Runnable 并管理它的聊天消息历史记录。\n",
    "\n",
    "具体来说，它可用于任何将以下之一作为输入的 Runnable\n",
    "\n",
    "- 一个字典，其键采用 BaseMessage 序列\n",
    "- 一个字典，其键将最新消息作为 BaseMessage 的字符串或序列，以及一个单独的键，将历史消息作为字符串或序列\n",
    "\n",
    "并作为输出之一返回\n",
    "\n",
    "- 可以视为 AIMessage 内容的字符串\n",
    "- 一个字典，其键包含 BaseMessage 序列\n",
    "\n",
    "让我们看一些示例，看看它是如何工作的。首先我们构造一个可运行的程序（这里接受一个字典作为输入并返回一条消息作为输出）：\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c5077929",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "from langchain_openai import ChatOpenAI, OpenAI\n",
    "\n",
    "openai_api_key = \"EMPTY\"\n",
    "openai_api_base = \"http://127.0.0.1:1234/v1\"\n",
    "model = ChatOpenAI(\n",
    "    openai_api_key=openai_api_key,\n",
    "    openai_api_base=openai_api_base,\n",
    "    temperature=0.3,\n",
    ")\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"你是一位擅长{ability}的助手。 用 20 个字或更少的字数进行简洁概要的回复\",\n",
    "        ),\n",
    "        MessagesPlaceholder(variable_name=\"history\"),\n",
    "        (\"human\", \"{input}\"),\n",
    "    ]\n",
    ")\n",
    "runnable = prompt | model"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d61bd95a",
   "metadata": {},
   "source": [
    "为了管理消息历史记录，我们需要： \n",
    "1. 这个可运行程序； \n",
    "2. 返回 BaseChatMessageHistory 实例的可调用函数。\n",
    "\n",
    "查看内存集成页面，了解使用 Redis 和其他提供程序实现聊天消息历史记录。在这里，我们演示如何使用内存中 ChatMessageHistory 以及使用 RedisChatMessageHistory 进行更持久的存储。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c12135f",
   "metadata": {},
   "source": [
    "## In-memory 内存中\n",
    "\n",
    "下面我们展示了一个简单的示例，其中聊天历史记录位于内存中，在本例中通过全局 Python 字典。\n",
    "\n",
    "我们构造一个可调用的 get_session_history ，它引用此字典以返回 ChatMessageHistory 的实例。可以通过在运行时将配置传递给 RunnableWithMessageHistory 来指定可调用的参数。默认情况下，配置参数应为单个字符串 session_id 。这可以通过 history_factory_config kwarg 进行调整。\n",
    "\n",
    "使用单参数默认值："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "eec4ed3d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.chat_message_histories import ChatMessageHistory\n",
    "from langchain_core.chat_history import BaseChatMessageHistory\n",
    "from langchain_core.runnables.history import RunnableWithMessageHistory\n",
    "\n",
    "store = {}\n",
    "\n",
    "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n",
    "    if session_id not in store:\n",
    "        store[session_id] = ChatMessageHistory()\n",
    "    return store[session_id]\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"input\",\n",
    "    history_messages_key=\"history\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e684df9f",
   "metadata": {},
   "source": [
    "请注意，我们指定了 input_messages_key （被视为最新输入消息的键）和 history_messages_key （添加历史消息的键）。\n",
    "\n",
    "当调用这个新的可运行时，我们通过配置参数指定相应的聊天历史记录："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "4ad19898",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='余弦是三角函数，表示直角三角形中角的正弦值的倒数。')"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"数学\", \"input\": \"余弦是什么意思？\"},\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "cb029342",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='余弦是三角函数，它是直角三角形中一个角的正弦值的倒数。')"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"能否再说一遍？\"},\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c6b749f7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='当然，我是擅长数学的助手，请告诉我您需要什么帮助。')"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# New session_id --> does not remember.\n",
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"能否再说一遍？\"},\n",
    "    config={\"configurable\": {\"session_id\": \"def234\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cdefd3f",
   "metadata": {},
   "source": [
    "我们可以通过将 ConfigurableFieldSpec 对象列表传递给 history_factory_config 参数来自定义用于跟踪消息历史记录的配置参数。下面，我们使用两个参数： user_id 和 conversation_id 。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "17af18ad",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import ConfigurableFieldSpec\n",
    "\n",
    "store = {}\n",
    "\n",
    "\n",
    "def get_session_history(user_id: str, conversation_id: str) -> BaseChatMessageHistory:\n",
    "    if (user_id, conversation_id) not in store:\n",
    "        store[(user_id, conversation_id)] = ChatMessageHistory()\n",
    "    return store[(user_id, conversation_id)]\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"input\",\n",
    "    history_messages_key=\"history\",\n",
    "    history_factory_config=[\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"user_id\",\n",
    "            annotation=str,\n",
    "            name=\"User ID\",\n",
    "            description=\"用户的唯一标识符。\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"conversation_id\",\n",
    "            annotation=str,\n",
    "            name=\"Conversation ID\",\n",
    "            description=\"对话的唯一标识符。\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "    ],\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "27ed7f19",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='你好！')"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"你好\"},\n",
    "    config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"1\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5889eabb",
   "metadata": {},
   "source": [
    "## 具有不同签名的可运行实例的示例\n",
    "\n",
    "上面的可运行程序接受一个字典作为输入并返回一个 BaseMessage。下面我们展示了一些替代方案。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "2f26e837",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content='二元一次方程是指含有两个未知数且每个未知数的最高次数为1的方程。它可以表示成ax + by = c的形式，其中a、b和c是常数，x和y是未知数。例如，方程3x + 2y = 7就是一个二元一次方程。')}"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "from langchain_core.runnables import RunnableParallel\n",
    "\n",
    "chain = RunnableParallel({\"output_message\":model})\n",
    "\n",
    "\n",
    "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n",
    "    if session_id not in store:\n",
    "        store[session_id] = ChatMessageHistory()\n",
    "    return store[session_id]\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    chain,\n",
    "    get_session_history,\n",
    "    output_messages_key=\"output_message\",\n",
    ")\n",
    "\n",
    "with_message_history.invoke(\n",
    "    [HumanMessage(content=\"什么是二元一次方程\")],\n",
    "    config={\"configurable\": {\"session_id\": \"baz\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "638bf57d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content='在数学中，未知数是指一个未知的量或变量。它通常用字母来表示，如x、y等，用来代表我们不知道的值。在方程或表达式中，未知数的值需要通过解题过程来确定。')}"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    [HumanMessage(content=\"什么是未知数\")],\n",
    "    config={\"configurable\": {\"session_id\": \"baz\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "c5c49e26",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "RunnableWithMessageHistory(bound=RunnableBinding(bound=RunnableBinding(bound=RunnableLambda(_enter_history), config={'run_name': 'load_history'})\n",
       "| RunnableBinding(bound=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x000002257EE97160>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x000002257EED7B80>, temperature=0.3, openai_api_key=SecretStr('**********'), openai_api_base='http://127.0.0.1:1234/v1', openai_proxy=''), config_factories=[<function Runnable.with_listeners.<locals>.<lambda> at 0x0000022518B7BF40>]), config={'run_name': 'RunnableWithMessageHistory'}), get_session_history=<function get_session_history at 0x0000022518BACB80>, history_factory_config=[ConfigurableFieldSpec(id='session_id', annotation=<class 'str'>, name='Session ID', description='Unique identifier for a session.', default='', is_shared=True, dependencies=None)])"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "RunnableWithMessageHistory(\n",
    "    model,\n",
    "    get_session_history,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "e8205fbe",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "RunnableWithMessageHistory(bound=RunnableBinding(bound=RunnableBinding(bound=RunnableAssign(mapper={\n",
       "  input_messages: RunnableBinding(bound=RunnableLambda(_enter_history), config={'run_name': 'load_history'})\n",
       "}), config={'run_name': 'insert_history'})\n",
       "| RunnableBinding(bound=RunnableLambda(itemgetter('input_messages'))\n",
       "  | ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x000002257EE97160>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x000002257EED7B80>, temperature=0.3, openai_api_key=SecretStr('**********'), openai_api_base='http://127.0.0.1:1234/v1', openai_proxy=''), config_factories=[<function Runnable.with_listeners.<locals>.<lambda> at 0x0000022518BAD480>]), config={'run_name': 'RunnableWithMessageHistory'}), get_session_history=<function get_session_history at 0x0000022518BACB80>, input_messages_key='input_messages', history_factory_config=[ConfigurableFieldSpec(id='session_id', annotation=<class 'str'>, name='Session ID', description='Unique identifier for a session.', default='', is_shared=True, dependencies=None)])"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "RunnableWithMessageHistory(\n",
    "    itemgetter(\"input_messages\") | model,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"input_messages\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "da51c0a5",
   "metadata": {},
   "source": [
    "## 持久存储\n",
    "\n",
    "在许多情况下，最好保留对话历史记录。 RunnableWithMessageHistory 不知道 get_session_history 可调用对象如何检索其聊天消息历史记录。有关使用本地文件系统的示例，请参阅此处。下面我们演示如何使用 Redis。查看内存集成页面，了解使用其他提供程序实现聊天消息历史记录。\n",
    "\n",
    "如果尚未安装 Redis，我们需要安装它：\n",
    "\n",
    "pip install --upgrade --quiet redis\n",
    "\n",
    "如果我们没有可连接的现有 Redis 部署，请启动本地 Redis Stack 服务器：\n",
    "\n",
    "docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest\n",
    "\n",
    "REDIS_URL = \"redis://localhost:6379/0\"\n",
    "\n",
    "更新消息历史记录实现只需要我们定义一个新的可调用对象，这次返回 RedisChatMessageHistory 的实例：\n",
    "\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8cf919e4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.chat_message_histories import RedisChatMessageHistory\n",
    "\n",
    "\n",
    "def get_message_history(session_id: str) -> RedisChatMessageHistory:\n",
    "    return RedisChatMessageHistory(session_id, url=REDIS_URL)\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,\n",
    "    get_message_history,\n",
    "    input_messages_key=\"input\",\n",
    "    history_messages_key=\"history\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23d63450",
   "metadata": {},
   "outputs": [],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n",
    "    config={\"configurable\": {\"session_id\": \"foobar\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9110029",
   "metadata": {},
   "outputs": [],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"ability\": \"math\", \"input\": \"What's its inverse\"},\n",
    "    config={\"configurable\": {\"session_id\": \"foobar\"}},\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
